/***************************************************************************//**
 * @file CGI/SkyDome.cpp
 * @author  Marek M. Cel <marekcel@mscsim.org>
 *
 * @section LICENSE
 *
 * Copyright (C) 2013 Marek M. Cel
 *
 * This file is part of MScSim. You can redistribute and modify it under
 * the terms of GNU General Public License as published by the Free Software
 * Foundation; either version 3 of the License, or (at your option) any later
 * version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
 * for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.
 * 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 *
 * Further information about the GNU General Public License can also be found
 * on the world wide web at http://www.gnu.org.
 ******************************************************************************/
#ifndef SKY_DOME_CPP
#define SKY_DOME_CPP
#endif

////////////////////////////////////////////////////////////////////////////////

#ifndef _USE_MATH_DEFINES
#   define _USE_MATH_DEFINES
#endif

////////////////////////////////////////////////////////////////////////////////

#include <cmath>
#include <iostream>

#include <osg/CoordinateSystemNode>
#include <osg/Geode>
#include <osg/Geometry>
#include <osg/Material>
#include <osg/Texture2D>

#include <osgDB/ReadFile>

#ifdef MSCSIM_OSGEPHEMERIS
#   include <osgEphemeris/EphemerisModel.h>
#endif

#include <CGI/NodeCallback.h>
#include <CGI/SkyDome.h>

////////////////////////////////////////////////////////////////////////////////

using namespace std;

////////////////////////////////////////////////////////////////////////////////

const double SkyDome::m_radius = 1000.0;

////////////////////////////////////////////////////////////////////////////////

SkyDome::SkyDome( CGI_Data *data ) :
    m_data ( data ),

    m_skyDomeSun ( 0 )
{
    m_skyDomeGroup = createSkyDomeGroup();

    m_skyDomeData = new DataTypeSkyDome( m_skyDomeGroup.get() );

    m_skyDomeGroup->setUserData( m_skyDomeData.get() );
    m_skyDomeGroup->setUpdateCallback( new NodeCallback<DataTypeSkyDome> );
}

////////////////////////////////////////////////////////////////////////////////

SkyDome::~SkyDome()
{
    if ( m_skyDomeLight ) delete m_skyDomeLight; m_skyDomeLight = 0;
    if ( m_skyDomeSun   ) delete m_skyDomeSun;   m_skyDomeSun   = 0;
}

////////////////////////////////////////////////////////////////////////////////

void SkyDome::update()
{
#   ifndef MSCSIM_OSGEPHEMERIS
    //////////////////////////
    m_skyDomeLight ->update();
    m_skyDomeSun   ->update();
    //////////////////////////
#   endif

#   ifdef MSCSIM_TEST_SKYDOME
    double lat = m_data->dataInp.aircraft.latitude;
    double lon = m_data->dataInp.aircraft.longitude;
    double alt = m_data->dataInp.aircraft.altitude;
#   else
    double lat = m_data->cameraData.latitude;
    double lon = m_data->cameraData.longitude;
    double alt = m_data->cameraData.altitude;
#   endif

    double azimuth = m_data->sunData.azimuth;

    osg::Quat  attitude;
    osg::Vec3d position;

    osg::ref_ptr<osg::EllipsoidModel> em = new osg::EllipsoidModel();

    osg::Matrixd T_wgs2enu;

    osg::Quat q_wgs2enu;
    osg::Quat q_enu2ned;

    double scale = 10.0;

    // distance to the horizon in kilometers (simplified formula)
    if ( alt > 0.0 ) scale = 3.6 * sqrt( alt );
    else             scale = 1.0;

    if ( scale <    1.0 ) scale =    1.0;
    if ( scale > 1000.0 ) scale = 1000.0;

    double x = 0.0;
    double y = 0.0;
    double z = 0.0;

#   ifndef MSCSIM_OSGEPHEMERIS
    em->convertLatLongHeightToXYZ( lat, lon, 0.0, x, y, z );
#   else
    // altitude correction due to Earth shape
    double cosLat = cos( lat );
    double a2 = osg::WGS_84_RADIUS_EQUATOR * osg::WGS_84_RADIUS_EQUATOR;
    double b2 = osg::WGS_84_RADIUS_POLAR   * osg::WGS_84_RADIUS_POLAR;
    double e2 = ( a2 - b2 ) / a2;
    double r2 = b2 / ( 1.0 - e2 * cosLat*cosLat );
    double r = sqrt( r2 );
    double alpha = atan2( scale * SkyDome::m_radius, r );
    double h_del = r - cos( alpha ) * r;

    em->convertLatLongHeightToXYZ( lat, lon, -h_del, x, y, z );
#   endif

    em->computeLocalToWorldTransformFromXYZ( x, y, z, T_wgs2enu );

    q_wgs2enu = T_wgs2enu.getRotate();

    q_enu2ned.makeRotate( M_PI    , osg::Vec3d( 1.0, 0.0, 0.0 ),
                          0.0     , osg::Vec3d( 0.0, 1.0, 0.0 ),
                         -azimuth , osg::Vec3d( 0.0, 0.0, 1.0 ) );

    attitude = q_enu2ned * q_wgs2enu;

    position = osg::Vec3d( x, y, z );

#   ifndef MSCSIM_OSGEPHEMERIS
    m_skyDomeData->setAttitude( attitude );
    m_skyDomeData->setScale( scale );
    m_skyDomeData->setPosition( position );
    m_skyDomeData->setElevation( m_data->sunData.elevation );
#   else
    m_skyDomeData->setAttitude( q_wgs2enu );
    m_skyDomeData->setScale( scale );
    m_skyDomeData->setPosition( position );

    Utils::DateTime dateTime;

    dateTime.year   = m_data->dataInp.dateTime.year;
    dateTime.month  = m_data->dataInp.dateTime.month;
    dateTime.day    = m_data->dataInp.dateTime.day;
    dateTime.hour   = m_data->dataInp.dateTime.hour;
    dateTime.minute = m_data->dataInp.dateTime.minute;
    dateTime.second = m_data->dataInp.dateTime.second;

    m_skyDomeData->setDateTime( dateTime );
    m_skyDomeData->setLatLonAlt( osg::RadiansToDegrees( lat ),
                                 osg::RadiansToDegrees( lon ),
                                 alt + h_del );

    m_data->sunData.elevation = m_skyDomeData->getSunElevation();
#   endif
}

////////////////////////////////////////////////////////////////////////////////

osg::ref_ptr<osg::Group> SkyDome::createSkyDomeGroup()
{
    osg::ref_ptr<osg::Group> skyDomeGroup = new osg::Group();
    skyDomeGroup->setName( "SkyDomeGroup" );

    osg::ref_ptr<osg::PositionAttitudeTransform> skyDomePosition = new osg::PositionAttitudeTransform();
    osg::ref_ptr<osg::PositionAttitudeTransform> skyDomeSizeCoef = new osg::PositionAttitudeTransform();
    osg::ref_ptr<osg::PositionAttitudeTransform> skyDomeAttitude = new osg::PositionAttitudeTransform();
    skyDomePosition->setName( "SkyDomePosition" );
    skyDomeSizeCoef->setName( "SkyDomeSizeCoef" );
    skyDomeAttitude->setName( "SkyDomeAttitude" );

    skyDomePosition->addChild( skyDomeSizeCoef.get() );
    skyDomeSizeCoef->addChild( skyDomeAttitude.get() );

#   ifndef MSCSIM_OSGEPHEMERIS
    m_skyDomeLight = new SkyDomeLight ( m_data );
    m_skyDomeSun   = new SkyDomeSun   ( m_data );

    skyDomeAttitude->addChild( createSkyDome().get() );
    skyDomeAttitude->addChild( m_skyDomeSun->getSkyDomeSunGroup().get() );
    skyDomeAttitude->addChild( m_skyDomeLight->getSkyDomeLightGroup().get() );
#   else
    osg::ref_ptr<osgEphemeris::EphemerisModel> ephemerisModel = new osgEphemeris::EphemerisModel();
    ephemerisModel->setName( "SkyDomeEphemeris" );

    ephemerisModel->setMembers( osgEphemeris::EphemerisModel::SUN_LIGHT_SOURCE
                              | osgEphemeris::EphemerisModel::MOON_LIGHT_SOURCE
                              | osgEphemeris::EphemerisModel::SKY_DOME
                              | osgEphemeris::EphemerisModel::MOON
                              | osgEphemeris::EphemerisModel::PLANETS
                              | osgEphemeris::EphemerisModel::STAR_FIELD );

    ephemerisModel->setAutoDateTime( false );
    ephemerisModel->setUseEphemerisEngine( true );
    ephemerisModel->setMoveWithEyePoint( false );

    ephemerisModel->setSkyDomeRadius( SkyDome::m_radius );

    ephemerisModel->setSunLightNum( 0 );
    ephemerisModel->setMoonLightNum( 1 );

    // 2 = quite a clear day
    // 20 = hazy day
    // Reasonable values are 1 - 60
    ephemerisModel->setTurbidity( 2.0 );

    skyDomeAttitude->addChild( ephemerisModel.get() );
#   endif

    skyDomeGroup->addChild( skyDomePosition.get() );

    return skyDomeGroup;
}

////////////////////////////////////////////////////////////////////////////////

osg::ref_ptr<osg::Geode> SkyDome::createSkyDome()
{
    osg::ref_ptr<osg::Geode> skyDomeGeode = new osg::Geode();
    skyDomeGeode->setName( "SkyDomeGeode" );

    m_vertices = new osg::Vec3Array();
    m_texCoors = new osg::Vec2Array();
    m_normals  = new osg::Vec3Array();

    osg::ref_ptr<osg::Geometry> skyDomeGeometry = new osg::Geometry();
    skyDomeGeometry->setName( "SkyDomeGeometry" );

    int latSteps = 18;
    int lonSteps = 36;

    double latStep = osg::DegreesToRadians( 180.0 ) / (double)latSteps;
    double lonStep = osg::DegreesToRadians( 360.0 ) / (double)lonSteps;

    double latCurr = osg::DegreesToRadians( 90.0 );
    double latNext = osg::DegreesToRadians( 90.0 ) - latStep;
    double lonCurr = 0.0;

    double xCurr = 0.0;
    double yCurr = 0.0;
    double zCurr = SkyDome::m_radius;

    double xNext = 0.0;
    double yNext = 0.0;
    double zNext = SkyDome::m_radius;

    double rCurr = 0.0;
    double rNext = 0.0;

    for ( int latIter = 0; latIter < latSteps; latIter++ )
    {
        if ( latNext < osg::DegreesToRadians( -90.0 ) )
        {
            latNext = osg::DegreesToRadians( -90.0 );
        }

        zCurr = SkyDome::m_radius * sin( latCurr );
        rCurr = SkyDome::m_radius * cos( latCurr );

        for ( int lonIter = 0; lonIter < lonSteps + 1; lonIter++ )
        {
            lonCurr = lonIter * lonStep;

            xCurr = rCurr * sin( lonCurr );
            yCurr = rCurr * cos( lonCurr );

            zNext = SkyDome::m_radius * sin( latNext );
            rNext = SkyDome::m_radius * cos( latNext );

            xNext = rNext * sin( lonCurr );
            yNext = rNext * cos( lonCurr );

            osg::Vec3d pntCurr( xCurr, yCurr, zCurr );
            osg::Vec3d pntNext( xNext, yNext, zNext );

            m_vertices->push_back( pntCurr );
            m_vertices->push_back( pntNext );

            m_texCoors->push_back( osg::Vec2( (double)lonIter/lonSteps, (double)latIter/latSteps ) );
            m_texCoors->push_back( osg::Vec2( (double)lonIter/lonSteps, (double)(latIter+1)/latSteps ) );

            pntCurr.normalize();
            pntNext.normalize();

            m_normals->push_back( -pntCurr );
            m_normals->push_back( -pntNext );
        }

        latCurr -= latStep;
        latNext -= latStep;
    }

    skyDomeGeometry->setNormalArray( m_normals.get() );
    skyDomeGeometry->setNormalBinding( osg::Geometry::BIND_PER_VERTEX );

    skyDomeGeometry->setVertexArray( m_vertices.get() );

    skyDomeGeometry->addPrimitiveSet( new osg::DrawArrays( osg::PrimitiveSet::QUAD_STRIP, 0, m_vertices->size() ) );

    // state set
    osg::ref_ptr<osg::StateSet> skyDomeStateSet = skyDomeGeode->getOrCreateStateSet();

    skyDomeStateSet->setMode( GL_LIGHTING, osg::StateAttribute::OFF );

    // texture
    std::string textureFilename = "data/textures/skydome/sky_night.png";
    osg::ref_ptr<osg::Image> skyDomeImage = osgDB::readImageFile( textureFilename );

    if ( skyDomeImage.valid() )
    {
        skyDomeGeometry->setTexCoordArray( 0, m_texCoors.get() );

        osg::ref_ptr<osg::Texture2D> skyDomeTexture = new osg::Texture2D();
        skyDomeTexture->setImage( skyDomeImage.get() );

        skyDomeTexture->setWrap( osg::Texture2D::WRAP_S, osg::Texture2D::MIRROR );
        skyDomeTexture->setWrap( osg::Texture2D::WRAP_T, osg::Texture2D::MIRROR );

        skyDomeTexture->setMaxAnisotropy( 8.0f );

        skyDomeStateSet->setTextureAttributeAndModes( 0, skyDomeTexture.get(), osg::StateAttribute::ON );
    }
    else
    {
        cerr << "ERROR! Cannot open texture file: " << textureFilename << endl;
    }

    // material
    osg::ref_ptr<osg::Material> skyDomeMaterial = new osg::Material();

    skyDomeMaterial->setColorMode( osg::Material::AMBIENT_AND_DIFFUSE );

    skyDomeMaterial->setAmbient( osg::Material::FRONT, osg::Vec4( 1.0f, 1.0f, 1.0f, 1.0f ) );
    skyDomeMaterial->setDiffuse( osg::Material::FRONT, osg::Vec4( 1.0f, 1.0f, 1.0f, 1.0f ) );

    skyDomeStateSet->setAttribute( skyDomeMaterial.get() );

    skyDomeGeode->addDrawable( skyDomeGeometry.get() );

    return skyDomeGeode;
}
